Skip to content

Fix phpstan/phpstan#14411: Incorrect type narrowing with dependent types#5368

Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-4qth8up
Closed

Fix phpstan/phpstan#14411: Incorrect type narrowing with dependent types#5368
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-4qth8up

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

PHPStan incorrectly reported "Strict comparison using === between mixed and null will always evaluate to false" when checking $a === null inside a $b !== null block, where $a and $b were assigned in different branches of an if/else and $b could be non-null independently of $a.

Changes

  • Added a guard compatibility check in MutatingScope::createConditionalExpressions() (src/Analyser/MutatingScope.php) that prevents creating conditional type narrowings when the guard variable's type in the other branch has any overlap with the guard type
  • The check is applied in both loops that create ConditionalExpressionHolder instances (for existing expressions and for merged-only expressions)

Root cause

When merging two branches of an if/else, createConditionalExpressions creates conditional type links between variables that differ across branches. For example, from:

if ($a !== null) { $b = $a; } else { $b = get_optional_int(); }

It would create: "if $b has type mixed~null, then $a has type mixed~null". This conditional is unsound because $b can be non-null from the else branch (when get_optional_int() returns an int), while $a is null in that branch.

The fix checks whether the guard type from our branch is incompatible with the other branch's type for the same variable. If there's any overlap (i.e., isSuperTypeOf is not no), the conditional is not created, because the guard being satisfied doesn't uniquely identify which branch was taken.

Test

Added tests/PHPStan/Analyser/nsrt/bug-14411.php that verifies $a retains type mixed (not mixed~null) inside the if ($b !== null) block.

Fixes phpstan/phpstan#14411

- Added guard compatibility check in createConditionalExpressions to prevent
  creating unsound conditional type narrowings when the guard variable's type
  in the other branch overlaps with the guard type
- New regression test in tests/PHPStan/Analyser/nsrt/bug-14411.php
- The root cause was that createConditionalExpressions created "if $b is X then
  $a is Y" conditionals even when $b could have type X from either branch,
  making the conditional unable to uniquely identify which branch was taken
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-4qth8up branch March 31, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants